package cn.wizzer.app.web.commons.doc;

import java.awt.image.BufferedImage;
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
import java.io.StringWriter;
import java.math.BigInteger;
import java.math.RoundingMode;
import java.net.URL;
import java.text.Normalizer;
import java.text.NumberFormat;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.Stack;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import com.microsoft.schemas.vml.CTTextbox;
import net.sf.json.JSONArray;
import org.apache.poi.ooxml.POIXMLDocument;
import org.apache.poi.util.Units;
import org.apache.poi.xwpf.usermodel.*;
import org.apache.xmlbeans.XmlCursor;
import org.apache.xmlbeans.XmlException;
import org.apache.xmlbeans.XmlObject;
import org.apache.xmlbeans.XmlOptions;
import org.nutz.lang.util.NutMap;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTBody;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTBookmark;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTPicture;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTText;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

import javax.imageio.ImageIO;
import javax.xml.namespace.QName;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;

/**
 *
 * Word 文件中标签的封装类，保存了其定义和内部的操作
 *
 * @author
 *
 * <p>Modification History:</p>
 * <p>Date       Author      Description</p>
 * <p>------------------------------------------------------------------</p>
 * <p> </p>
 * <p>  </p>
 */
public class BookMark {

    //以下为定义的常量

    /** 替换标签时，设于标签的后面   **/
    public static final int INSERT_AFTER = 0;

    /** 替换标签时，设于标签的前面   **/
    public static final int INSERT_BEFORE = 1;

    /** 替换标签时，将内容替换书签   **/
    public static final int REPLACE = 2;

    /** docx中定义的部分常量引用  **/
    public static final String RUN_NODE_NAME = "w:r";
    public static final String TEXT_NODE_NAME = "w:t";
    public static final String BOOKMARK_START_TAG = "bookmarkStart";
    public static final String BOOKMARK_END_TAG = "bookmarkEnd";
    public static final String BOOKMARK_ID_ATTR_NAME = "w:id";
    public static final String BOOKMARK_NAME_ATTR_NAME = "w:name";
    public static final String STYLE_NODE_NAME = "w:rPr";

    /** 内部的标签定义类  **/
    private CTBookmark _ctBookmark = null;

    /** 标签所处的段落  **/
    private XWPFParagraph _para = null;

    /** 标签所在的表cell对象  **/
    private XWPFTableCell _tableCell = null;

    /** 标签所在的表cell对象  **/
    private CTPicture _picture = null;

    /** 标签名称 **/
    private String _bookmarkName = null;

    /** 该标签是否处于表格内  **/
    private boolean _isCell = false;

    /** 该标签是否处于表格内  **/
    private boolean _isTextBox = false;

    /**
     * 构造函数
     * @param ctBookmark
     * @param para
     */
    public BookMark(CTBookmark ctBookmark, XWPFParagraph para) {
        this._ctBookmark = ctBookmark;
        this._para = para;
        //this._bookmarkName = ctBookmark.getName();
        this._bookmarkName = getBookmarkName(ctBookmark);
        this._tableCell = null;
        this._isCell = false;
        this._picture =null;
        this._isTextBox=false;
    }

    /**
     * 构造函数，用于表格中的标签
     * @param ctBookmark
     * @param para
     * @param tableCell
     */
    public BookMark(CTBookmark ctBookmark, XWPFParagraph para, XWPFTableCell tableCell) {
        this(ctBookmark, para);
        this._tableCell = tableCell;
        this._isCell = true;
        this._picture =null;
        this._isTextBox=false;
    }


    /**
     * 构造函数,用于文本框中的标签
     * @param ctBookmark
     * @param para
     */
    public BookMark(CTBookmark ctBookmark, XWPFParagraph para, CTPicture picture) {
        this._ctBookmark = ctBookmark;
        this._para = para;
        //this._bookmarkName = ctBookmark.getName();
        this._bookmarkName = getBookmarkName(ctBookmark);
        this._tableCell = null;
        this._isCell = false;
        this._picture = picture;
        this._isTextBox=true;
    }

    public boolean isInTable() {
        return this._isCell;
    }

    public XWPFTable getContainerTable() {
        return this._tableCell.getTableRow().getTable();
    }

    public XWPFTableRow getContainerTableRow() {
        return this._tableCell.getTableRow();
    }

    public String getBookmarkName() {
        return  this._bookmarkName;
    }

    /**
     * Insert text into the Word document in the location indicated by this
     * bookmark.
     *
     * @param bookmarkValue An instance of the String class that encapsulates
     * the text to insert into the document.
     * @param where A primitive int whose value indicates where the text ought
     * to be inserted. There are three options controlled by constants; insert
     * the text immediately in front of the bookmark (Bookmark.INSERT_BEFORE),
     * insert text immediately after the bookmark (Bookmark.INSERT_AFTER) and
     * replace any and all text that appears between the bookmark's square
     * brackets (Bookmark.REPLACE).
     */
    public void insertTextAtBookMark(Object bookmarkValue, int where) throws Exception{

        //根据标签的类型，进行不同的操作
        if(this._isCell) {
            this.handleBookmarkedCells(bookmarkValue, where);
        }else if(this._isTextBox){
            this.handleBookmarkedTextBox(bookmarkValue,where);
        }else {
            handleBookmarkedPara(bookmarkValue, where);
        }
    }

    /**
     *在Word文档中立即插入一些文本
     *在一个命名书签之后。
     *
     *书签可以有两种形式，它们可以简单地标记一个位置
     *在文档中，或者它们可以这样做，但包含一些文本。这个
     *通过查看一些XML标记，差异是显而易见的。简单的
     *占位符书签看起来像这样；
     *
     *<上一页>
     *
     *<w:bookmarkStart w:name=“AllAlone”w:id=“0”/><w:书签结束w:id=“0”/>
     *
     *</pre>
     *
     *只需一对标签，其中一个标签的名称为bookmarkStart，另一个标签
     *名称bookmarkEnd和两者共享匹配的id属性。在这种情况下，
     *文本将立即插入到文档中
     *在书签结束标签之后。文本不会应用任何样式，它
     *将简单地继承文档默认值。
     *
     *更复杂的情况是这样的；
     *
     *<上一页>
     *
     *<w:bookmarkStart w:name=“InStyledText”w:id=“3”/>
     *<w:r w:rsidRPr=“00DA438C”>
     *<w:rPr>
     *<w:rFonts w:hAnsi=“雕刻机MT”w:ascii=“雕刻商MT”w:cs=“Arimo”/>
     *<w:color w:val=“FF0000”/>
     *</w:rPr>
     *<w:t>文本</w:t>
     *</w:r>
     *<w:bookmarkEnd w:id=“3”/>
     *
     *</pre>
     *
     *在这里，用户选择了单词“text”，并选择插入
     *此时将书签添加到文档中。因此，书签标签“包含”
     *有风格的角色奔跑。在此书签后插入任何文本，
     *确保样式得以保留和复制非常重要
     *新插入的文本。
     *
     *处理这两起案件的方法相似，但略有不同
     *不同。在这两种情况下，代码都只是沿着文档节点逐步执行
     *直到它找到与ID匹配的bookmarkEnd标记
     *书签开始标签。然后，它将查看是否还有另一个节点
     *在bookmarkEnd标签后面。如果有，它将把文本插入
     *紧挨着这个节点前面的段落。另一方面，
     *bookmarkEnd标记后没有更多节点，然后新运行
     *将简单地放在段落末尾。
     *
     *样式是通过在迭代时“查找”“w:rPr”元素来处理的
     *通过节点。如果发现一个，将捕获其详细信息，并
     *在将游程插入段落之前应用于游程。如果
     *bookmarkStart和bookmarkEnd标签之间有多次运行
     *这些应用了不同的样式，然后应用了样式
     *到bookmarkEnd标签（如果有的话）被克隆之前的最后一次运行
     *应用于新插入的文本。
     *
     *@param run XWPFRun类的一个实例，用于封装文本
     *该内容将被插入到书签后面的文档中。
     */
    private void insertAfterBookmark(XWPFRun run) {
        Node nextNode = null;
        Node insertBeforeNode = null;
        Node styleNode = null;
        int bookmarkStartID = 0;
        int bookmarkEndID = -1;

        // Capture the id of the bookmarkStart tag. The code will step through
        // the document nodes 'contained' within the start and end tags that have
        // matching id numbers.
        bookmarkStartID = this.getBookmarkId(this._ctBookmark);

        // Get the node for the bookmark start tag and then enter a loop that
        // will step from one node to the next until the bookmarkEnd tag with
        // a matching id is fouind.
        nextNode = this._ctBookmark.getDomNode();
        while (bookmarkStartID != bookmarkEndID) {

            // Get the next node along and check to see if it is a bookmarkEnd
            // tag. If it is, get its id so that the containing while loop can
            // be terminated once the correct end tag is found. Note that the
            // id will be obtained as a String and must be converted into an
            // integer. This has been coded to fail safely so that if an error
            // is encuntered converting the id to an int value, the while loop
            // will still terminate.
            nextNode = nextNode.getNextSibling();
            if (nextNode.getNodeName().contains(BookMark.BOOKMARK_END_TAG)) {
                try {
                    bookmarkEndID = Integer.parseInt(
                            nextNode.getAttributes().getNamedItem(
                                    BookMark.BOOKMARK_ID_ATTR_NAME).getNodeValue());
                } catch (NumberFormatException nfe) {
                    bookmarkEndID = bookmarkStartID;
                }
            } // If we are not dealing with a bookmarkEnd node, are we dealing
            // with a run node that MAY contains styling information. If so,
            // then get that style information from the run.
            else {
                if (nextNode.getNodeName().equals(BookMark.RUN_NODE_NAME)) {
                    styleNode = this.getStyleNode(nextNode);
                }
            }
        }

        // After the while loop completes, it should have located the correct
        // bookmarkEnd tag but we cannot perform an insert after only an insert
        // before operation and must, therefore, get the next node.
        insertBeforeNode = nextNode.getNextSibling();

        // Style the newly inserted text. Note that the code copies or clones
        // the style it found in another run, failure to do this would remove the
        // style from one node and apply it to another.
        if (styleNode != null) {
            run.getCTR().getDomNode().insertBefore(
                    styleNode.cloneNode(true), run.getCTR().getDomNode().getFirstChild());
        }

        // Finally, check to see if there was a node after the bookmarkEnd
        // tag. If there was, then this code will insert the run in front of
        // that tag. If there was no node following the bookmarkEnd tag then the
        // run will be inserted at the end of the paragarph and this was taken
        // care of at the point of creation.
        if (insertBeforeNode != null) {
            this._para.getCTP().getDomNode().insertBefore(
                    run.getCTR().getDomNode(), insertBeforeNode);
        }
    }

    /**
     * Inserts some text into a Word document immediately in front of the
     * location of a bookmark.
     *
     * This case is slightly more straightforward than inserting after the
     * bookmark. For example, it is possible only to insert a new node in front
     * of an existing node. When inserting after the bookmark, then end node had
     * to be located whereas, in this case, the node is already known, it is the
     * CTBookmark itself. The only information that must be discovered is
     * whether there is a run immediately in front of the boookmarkStart tag and
     * whether that run is styled. If there is and if it is, then this style
     * must be cloned and applied the text which will be inserted into the
     * paragraph.
     *
     * @param run An instance of the XWPFRun class that encapsulates the text
     * that is to be inserted into the document following the bookmark.
     */
    private void insertBeforeBookmark(XWPFRun run) {
        Node insertBeforeNode = null;
        Node childNode = null;
        Node styleNode = null;

        // Get the dom node from the bookmarkStart tag and look for another
        // node immediately preceding it.
        insertBeforeNode = this._ctBookmark.getDomNode();
        childNode = insertBeforeNode.getPreviousSibling();

        // If a node is found, try to get the styling from it.
        if (childNode != null) {
            styleNode = this.getStyleNode(childNode);

            // If that previous node was styled, then apply this style to the
            // text which will be inserted.
            if (styleNode != null) {
                run.getCTR().getDomNode().insertBefore(
                        styleNode.cloneNode(true), run.getCTR().getDomNode().getFirstChild());
            }
        }

        // Insert the text into the paragraph immediately in front of the
        // bookmarkStart tag.
        this._para.getCTP().getDomNode().insertBefore(
                run.getCTR().getDomNode(), insertBeforeNode);
    }

    /**
     * Replace the text - if any - contained between the bookmarkStart and it's
     * matching bookmarkEnd tag with the text specified. The technique used will
     * resemble that employed when inserting text after the bookmark. In short,
     * the code will iterate along the nodes until it encounters a matching
     * bookmarkEnd tag. Each node encountered will be deleted unless it is the
     * final node before the bookmarkEnd tag is encountered and it is a
     * character run. If this is the case, then it can simply be updated to
     * contain the text the users wishes to see inserted into the document. If
     * the last node is not a character run, then it will be deleted, a new run
     * will be created and inserted into the paragraph between the bookmarkStart
     * and bookmarkEnd tags.
     *
     * @param run An instance of the XWPFRun class that encapsulates the text
     * that is to be inserted into the document following the bookmark.
     */
    private void replaceBookmark(XWPFRun run) {
        Node nextNode = null;
        Node styleNode = null;
        Node lastRunNode = null;
//        Node toDelete = null;
//        NodeList childNodes = null;
        Stack<Node> nodeStack = null;
//        boolean textNodeFound = false;
//        boolean foundNested = true;
        int bookmarkStartID = 0;
        int bookmarkEndID = -1;
//        int numChildNodes = 0;

        nodeStack = new Stack<Node>();
        //bookmarkStartID = this.getBookmarkId(this._ctBookmark);
        bookmarkStartID = this.getBookmarkId(this._ctBookmark);
        nextNode = this._ctBookmark.getDomNode();
        nodeStack.push(nextNode);

        // Loop through the nodes looking for a matching bookmarkEnd tag
        while (bookmarkStartID != bookmarkEndID) {
            nextNode = nextNode.getNextSibling();
            nodeStack.push(nextNode);

            // If an end tag is found, does it match the start tag? If so, end
            // the while loop.
            if (nextNode.getNodeName().contains(BookMark.BOOKMARK_END_TAG)) {
                try {
                    bookmarkEndID = Integer.parseInt(
                            nextNode.getAttributes().getNamedItem(
                                    BookMark.BOOKMARK_ID_ATTR_NAME).getNodeValue());
                } catch (NumberFormatException nfe) {
                    bookmarkEndID = bookmarkStartID;
                }
            }
            //else {
            // Place a reference to the node on the nodeStack
            //    nodeStack.push(nextNode);
            //}
        }

        // If the stack of nodes found between the bookmark tags is not empty
        // then they have to be removed.
        if (!nodeStack.isEmpty()) {

            // Check the node at the top of the stack. If it is a run, get it's
            // style - if any - and apply to the run that will be replacing it.
            //lastRunNode = nodeStack.pop();
            lastRunNode = nodeStack.peek();

            if ((lastRunNode.getNodeName().equals(BookMark.RUN_NODE_NAME))) {
                styleNode = this.getStyleNode(lastRunNode);
                if (styleNode != null) {
                    run.getCTR().getDomNode().insertBefore(
                            styleNode.cloneNode(true), run.getCTR().getDomNode().getFirstChild());
                }
            }

            // Delete any and all node that were found in between the start and
            // end tags. This is slightly safer that trying to delete the nodes
            // as they are found while stepping through them in the loop above.

            // If we are peeking, then this line can be commented out.
            //this._para.getCTP().getDomNode().removeChild(lastRunNode);
            this.deleteChildNodes(nodeStack);
        }

        // Place the text into position, between the bookmark tags.
        this._para.getCTP().getDomNode().insertBefore(
                run.getCTR().getDomNode(), nextNode);
    }

    /**
     * When replacing the bookmark's text, it is necessary to delete any nodes
     * that are found between matching start and end tags. Complications occur
     * here because it is possible to have bookmarks nested within bookmarks to
     * almost any level and it is important to not remove any inner or nested
     * bookmarks when replacing the contents of an outer or containing
     * bookmark. This code successfully handles the simplest occurrence - where
     * one bookmark completely contains another - but not more complex cases
     * where one bookmark overlaps another in the markup. That is still to do.
     *
     * @param nodeStack An instance of the Stack class that encapsulates
     * references to any and all nodes found between the opening and closing
     * tags of a bookmark.
     */
    private void deleteChildNodes(Stack<Node> nodeStack) {
        Node toDelete = null;
        int bookmarkStartID = 0;
        int bookmarkEndID = 0;
        boolean inNestedBookmark = false;

        // The first element in the list will be a bookmarkStart tag and that
        // must not be deleted.
        for(int i = 1; i < nodeStack.size(); i++) {

            // Get an element. If it is another bookmarkStart tag then
            // again, we do not want to delete it, it's matching end tag
            // or any nodes that fall inbetween.
            toDelete = nodeStack.elementAt(i);
            if(toDelete.getNodeName().contains(BookMark.BOOKMARK_START_TAG)) {
                bookmarkStartID = Integer.parseInt(
                        toDelete.getAttributes().getNamedItem(BookMark.BOOKMARK_ID_ATTR_NAME).getNodeValue());
                inNestedBookmark = true;
            }
            else if(toDelete.getNodeName().contains(BookMark.BOOKMARK_END_TAG)) {
                bookmarkEndID = Integer.parseInt(
                        toDelete.getAttributes().getNamedItem(BookMark.BOOKMARK_ID_ATTR_NAME).getNodeValue());
                if(bookmarkEndID == bookmarkStartID) {
                    inNestedBookmark = false;
                }
            }
            else {
                if(!inNestedBookmark) {
                    this._para.getCTP().getDomNode().removeChild(toDelete);
                }
            }
        }
    }

    /**
     * Recover styling information - if any - from another document node. Note
     * that it is only possible to accomplish this if the node is a run (w:r)
     * and this could be tested for in the code that calls this method. However,
     * a check is made in the calling code as to whether a style has been found
     * and only if a style is found is it applied. This method always returns
     * null if it does not find a style making that checking process easier.
     *
     * @param parentNode An instance of the Node class that encapsulates a
     * reference to a document node.
     * @return An instance of the Node class that encapsulates the styling
     * information applied to a character run. Note that if no styling
     * information is found in the run OR if the node passed as an argument to
     * the parentNode parameter is NOT a run, then a null value will be
     * returned.
     */
    private Node getStyleNode(Node parentNode) {
        Node childNode = null;
        Node styleNode = null;
        if (parentNode != null) {

            // If the node represents a run and it has child nodes then
            // it can be processed further. Note, whilst testing the code, it
            // was observed that although it is possible to get a list of a nodes
            // children, even when a node did have children, trying to obtain this
            // list would often return a null value. This is the reason why the
            // technique of stepping from one node to the next is used here.
            if (parentNode.getNodeName().equalsIgnoreCase(BookMark.RUN_NODE_NAME)
                    && parentNode.hasChildNodes()) {

                // Get the first node and catch it's reference for return if
                // the first child node is a style node (w:rPr).
                childNode = parentNode.getFirstChild();
                if (childNode.getNodeName().equals("w:rPr")) {
                    styleNode = childNode;
                } else {
                    // If the first node was not a style node and there are other
                    // child nodes remaining to be checked, then step through
                    // the remaining child nodes until either a style node is
                    // found or until all child nodes have been processed.
                    while ((childNode = childNode.getNextSibling()) != null) {
                        if (childNode.getNodeName().equals(BookMark.STYLE_NODE_NAME)) {
                            styleNode = childNode;
                            // Note setting to null here if a style node is
                            // found in order order to terminate any further
                            // checking
                            childNode = null;
                        }
                    }
                }
            }
        }
        return (styleNode);
    }

    /**
     *获取此书签封装的文本（如果有的话）
     *Word文档可以选择一个或多个文本项，然后
     *在那个位置插入书签。突出显示的文本将出现
     *在表示书签位置的方括号之间
     *文档的文本，它们将通过调用此方法返回。
     *
     *@return String类的一个实例，封装任何文本
     *出现在与以下内容相关的打开和关闭方括号之间
     *这个书签。
     *@throws XmlException在解析XML时遇到问题时抛出
     *从文档中恢复标记以构建CTText
     *获取书签文本可能需要的实例。
     */
    public String getBookmarkText() throws XmlException {
        StringBuilder builder = null;
        //我们是在处理一个有书签的表格单元格吗？如果是这样，整个电池的内容物（如果有的话）必须被回收并归还。
        if(this._tableCell != null) {
            builder = new StringBuilder(this._tableCell.getText());
        }
        else {
            builder = this.getTextFromBookmark();
        }
        return(builder == null ? null : builder.toString());
    }

    /**
     *书签有两种类型。一个是简单的占位符，而
     *第二个仍然是一个占位符，但它“包含”了一些文本。在第二个
     *例如，文档的创建者已经选择了一些文本，然后
     *选择在那里插入书签，如果差异明显
     *查看XML标记。
     *
     *简单的案例；
     *
     *<上一页>
     *
     *<w:bookmarkStart w:name=“AllAlone”w:id=“0”/><w:书签结束w:id=“0”/>
     *
     *</pre>
     *
     *更复杂的案件；
     *
     *<上一页>
     *
     *<w:bookmarkStart w:name=“InStyledText”w:id=“3”/>
     *<w:r w:rsidRPr=“00DA438C”>
     *<w:rPr>
     *<w:rFonts w:hAnsi=“雕刻机MT”w:ascii=“雕刻商MT”w:cs=“Arimo”/>
     *<w:color w:val=“FF0000”/>
     *</w:rPr>
     *<w:t>文本</w:t>
     *</w:r>
     *<w:bookmarkEnd w:id=“3”/>
     *
     *</pre>
     *
     *此方法假定用户希望从任何
     *出现在匹配对之间的标记中的字符运行
     *bookmarkStart和bookmarkEnd标签；因此再次使用上述示例，
     *此方法将向用户返回字符串“text”。这是可能的
     *但是，书签包含多个跑步记录，书签包含
     *包含其他书签。在这两种情况下，此代码都将返回
     *XML标记中出现的任何和所有运行中包含的文本
     *在匹配的bookmarkStart和bookmarkEnd标签之间。术语“匹配
     *bookmarkStart和bookmarkEndtags在这里是指id属性为的标签
     *具有匹配值。
     *
     *@return封装文本的StringBuilder类的实例
     *从书签之间的任何字符运行元素中恢复
     *开始和结束标签。如果找不到文本，则将为空值
     *返回。
     *@throws XmlException在解析XML时遇到问题时抛出
     *从文档中恢复标记以构建CTText
     *获取书签文本可能需要的实例。
     */
    private StringBuilder getTextFromBookmark() throws XmlException {
        int startBookmarkID = 0;
        int endBookmarkID = -1;
        Node nextNode = null;
//        Node childNode = null;
//        CTText text = null;
        StringBuilder builder = null;
//        String rawXML = null;

        // Get the ID of the bookmark from it's start tag, the DOM node from the
        // bookmark (to make looping easier) and initialise the StringBuilder.
        startBookmarkID = this.getBookmarkId(this._ctBookmark);
        nextNode = this._ctBookmark.getDomNode();
        builder = new StringBuilder();

        // Loop through the nodes held between the bookmark's start and end
        // tags.
        while (startBookmarkID != endBookmarkID) {

            // Get the next node and, if it is a bookmarkEnd tag, get it's ID
            // as matching ids will terminate the while loop..
            nextNode = nextNode.getNextSibling();
            if (nextNode.getNodeName().contains(BookMark.BOOKMARK_END_TAG)) {

                // Get the ID attribute from the node. It is a String that must
                // be converted into an int. An exception could be thrown and so
                // the catch clause will ensure the loop ends neatly even if the
                // value might be incorrect. Must inform the user.
                try {
                    endBookmarkID = Integer.parseInt(
                            nextNode.getAttributes().
                                    getNamedItem(BookMark.BOOKMARK_ID_ATTR_NAME).getNodeValue());
                } catch (NumberFormatException nfe) {
                    endBookmarkID = startBookmarkID;
                }
            } else {
                // This is not a bookmarkEnd node and can processed it for any
                // text it may contain. Note the check for both type - it must
                // be a run - and contain children. Interestingly, it seems as
                // though the node may contain children and yet the call to
                // nextNode.getChildNodes() will still return an empty list,
                // hence the need to step through the child nodes.
                if (nextNode.getNodeName().equals(BookMark.RUN_NODE_NAME)
                        && nextNode.hasChildNodes()) {
                    // Get the text from the child nodes.
                    builder.append(this.getTextFromChildNodes(nextNode));
                }
            }
        }
        return (builder);
    }

    /**
     *遍历Node的所有和任何子节点，其引用将为
     *作为参数传递给节点参数，并恢复以下内容
     *任何文本节点。测试表明，一个节点可以称为文本节点
     *然而，将其类型报告为不同的元素节点
     *例如。在文本节点上调用getNodeValue（）方法将返回
     *节点封装但在元素节点上执行相同操作的文本将
     *没有。事实上，调用只会返回一个空值。因此，这
     *方法将测试节点名称以捕获所有文本节点，即那些
     *name是'w:t'，然后是type。如果报告类型为文本
     *节点，获取其内容是一项微不足道的任务。但是，如果类型
     *如果未报告为文本类型，则有必要解析原始XML
     *标记节点以恢复其价值。
     *
     *@param node node类的一个实例，它封装了一个引用
     *到从正在处理的文档中恢复的节点。应该是
     *传递了对字符run-'w:r'-节点的引用。
     *@return封装文本的String类的实例
     *如果它们是文本节点，则从节点子节点中恢复。
     *@throws XmlException在解析XML时遇到问题时抛出
     *从文档中恢复标记以构建CTText
     *获取书签文本可能需要的实例。
     */
    private String getTextFromChildNodes(Node node) throws XmlException {
        NodeList childNodes = null;
        Node childNode = null;
        CTText text = null;
        StringBuilder builder = new StringBuilder();
        int numChildNodes = 0;

        // Get a list of chid nodes from the node passed to the method and
        // find out how many children there are in the list.
        childNodes = node.getChildNodes();
        numChildNodes = childNodes.getLength();

        // Iterate through the children one at a time - it is possible for a
        // run to ciontain zero, one or more text nodes - and recover the text
        // from an text type child nodes.
        for (int i = 0; i < numChildNodes; i++) {

            // Get a node and check it's name. If this is 'w:t' then process as
            // text type node.
            childNode = childNodes.item(i);

            if (childNode.getNodeName().equals(BookMark.TEXT_NODE_NAME)) {

                // If the node reports it's type as txet, then simply call the
                // getNodeValue() method to get at it's text.
                if (childNode.getNodeType() == Node.TEXT_NODE) {
                    builder.append(childNode.getNodeValue());
                } else {
                    // Correct the type by parsing the node's XML markup and
                    // creating a CTText object. Call the getStringValue()
                    // method on that to get the text.
                    text = CTText.Factory.parse(childNode);
                    builder.append(text.getStringValue());
                }
            }
        }
        return (builder.toString());
    }


    private void handleBookmarkedPara(Object bookmarkObject, int where) throws Exception {
        String bookmarkValue = null;
        if(bookmarkObject instanceof String){
            bookmarkValue = bookmarkObject.toString();
        }
        File file = new File(bookmarkValue);
        FileInputStream fis = null;
        int picType = getPicType(bookmarkValue);
        XWPFRun run = null;
        if(picType>0&&file.exists()){//处理普通段落中的图片书签
            // file = getFileByPath(bookmarkValue);获取网络路径图片
            fis = new FileInputStream(file);
            BufferedImage bufferedImg = ImageIO.read(file);
            int height = bufferedImg.getHeight();
            int width = bufferedImg.getWidth();
            for (XWPFRun runp : this._para.getRuns()) {
                runp.addPicture(fis,picType,bookmarkValue, Units.toEMU(width),Units.toEMU(height));
            }
            //run = this._para.createRun();
            //run.addPicture(fis,picType,bookmarkValue, Units.toEMU(width),Units.toEMU(height));
            fis.close();
        }else {
            //普通标签，直接创建一个元素
            run = this._para.createRun();
            run.setText(bookmarkValue);
            switch (where) {
                case BookMark.INSERT_AFTER:
                    this.insertAfterBookmark(run);
                    break;
                case BookMark.INSERT_BEFORE:
                    this.insertBeforeBookmark(run);
                    break;
                case BookMark.REPLACE:
                    this.replaceBookmark(run);
                    break;
                //}
            }
        }
    }

    private void handleBookmarkedCells(Object bookmarkObject, int where) throws Exception {
        List<XWPFParagraph> paraList = null;
//        List<XWPFRun> runs = null;
        XWPFParagraph para = null;
        XWPFRun readRun = null;
        FileInputStream fis = null;
        InputStream is = null;
        String bookmarkValue = null;
        if(bookmarkObject instanceof String){
            bookmarkValue = bookmarkObject.toString();
        }
        File file = new File(bookmarkValue);
        // Get a list if paragraphs from the table cell and remove any and all.
        paraList = this._tableCell.getParagraphs();
        for(int i = 0; i < paraList.size(); i++) {
            para = paraList.get(i);
            //this._tableCell.removeParagraph(i);
            int picType = getPicType(bookmarkValue);
            if(picType>0&&file.exists()){
               // file = getFileByPath(bookmarkValue);获取网络路径图片
                fis = new FileInputStream(file);
                BufferedImage bufferedImg = ImageIO.read(file);
                //160 68
                //120 51  4.23厘米
                Double th = this._tableCell.getTableRow().getHeight()*0.001768519-0.05;//厘米 0.05是预留边距
                Double ih = 4.23/120 * bufferedImg.getHeight(); //1像素多少厘米*像素得到厘米
                int height = (int)(bufferedImg.getHeight()* th/ih);
                int width = (int)(bufferedImg.getWidth()* th/ih);

                readRun = para.createRun();
                readRun.addPicture(fis,picType,bookmarkValue, Units.toEMU(width),Units.toEMU(height));
                //readRun.addPicture(fis,picType,bookmarkValue, Units.toEMU(tableHeight),Units.toEMU(tableWidth));
                fis.close();
            }else{
                List<NutMap> texts =  findSupSub(bookmarkValue);
                for(NutMap map :texts){
                    readRun = para.createRun();
                    int type = Integer.valueOf(map.get("type").toString());
                    String str = map.get("value").toString();
                    if(type == 1){
                        setSuPscript(para,readRun,str);
                    }else if(type == 2){
                        setSubscript(para,readRun,str);
                    }else{
                        setRun(para,readRun,str);
                    }
                }
            }
            /*XWPFParagraph para2 = this._tableCell.addParagraph();
            para2.createRun().setText(bookmarkValue);*/
        }
    }

    private void handleBookmarkedTextBox(Object bookmarkObject, int where) throws Exception {
        List<String> bookArray = new ArrayList<>();
        if(bookmarkObject instanceof String){
            bookArray.add(bookmarkObject.toString());
        }
        if(bookmarkObject instanceof List){
            bookArray.addAll((List<String>)bookmarkObject);
        }
        List<XWPFRun> runs = new ArrayList<>();
        for(String bookmarkValue:bookArray){
            XWPFRun run = this._para.createRun();
            File file = new File(bookmarkValue);
            FileInputStream fis = null;
            int picType = getPicType(bookmarkValue);
            if(picType>0&&file.exists()){//处理普通段落中的图片书签
                fis = new FileInputStream(file);
                BufferedImage bufferedImg = ImageIO.read(file);
                int height = bufferedImg.getHeight();
                int width = bufferedImg.getWidth();
                run.addPicture(fis,picType,bookmarkValue, Units.toEMU(width),Units.toEMU(height));
                fis.close();
            }else {
                run.setText(run.getPictureText()+bookmarkValue);
            }
            runs.add(run);
        }
        doTextBox(this._para.getCTP().getDomNode(),runs);
        this._para.getDocument();
    }

    public void doTextBox(Node domNode, List<XWPFRun> runs){
        try {
            // 查找所有 bookmarkStart 和 bookmarkEnd
            Node currentNode = domNode.getFirstChild();
            /*Node styleNode = null;
            Node lastRunNode = null;
            Stack<Node> nodeStack = null;
            nodeStack = new Stack<Node>();*/
            while (currentNode != null) {
                if (currentNode.getNodeName().contains(BookMark.BOOKMARK_START_TAG)) {
                    //文本框的样式不填了，麻烦死了
                   // nodeStack.push(domNode.get);
                    /*if (!nodeStack.isEmpty()) {
                        lastRunNode = nodeStack.peek();
                        if ((lastRunNode.getNodeName().equals(BookMark.RUN_NODE_NAME))) {
                            styleNode = this.getStyleNode(lastRunNode);
                            if (styleNode != null) {
                                run.getCTR().getDomNode().insertBefore(
                                        styleNode.cloneNode(true), run.getCTR().getDomNode().getFirstChild());
                            }
                        }
                        this.deleteChildNodes(nodeStack);
                    }*/
                    String bookname = currentNode.getAttributes().getNamedItem(BookMark.BOOKMARK_NAME_ATTR_NAME).getNodeValue();
                    if(this._bookmarkName.equals(bookname)){
                        for(XWPFRun run: runs) {
                            domNode.insertBefore(run.getCTR().getDomNode(), currentNode);
                        }
                    }
                } else if (currentNode.getNodeName().contains(BookMark.BOOKMARK_END_TAG)) {
                    // 处理 bookmarkEnd
                }
                currentNode = currentNode.getNextSibling();
            }
            if(domNode.getChildNodes().getLength()>0) {
                for(int i =0 ;i<domNode.getChildNodes().getLength();i++){
                    doTextBox(domNode.getChildNodes().item(i),runs);
                }
            }
        }catch (Exception e) {
            e.printStackTrace();
        }
    }
    //常规字符写入
    public static void setRun (XWPFParagraph para,XWPFRun readRun,String text){
        readRun.setText(text);
        readRun.getCTR().addNewRPr().addNewRFonts();
        readRun.getCTR().getRPr().addNewSz();
        readRun.getCTR().getRPr().addNewSzCs();
        if (para.getCTP()!=null&&para.getCTP().getPPr()!=null&&para.getCTP().getPPr().getRPr() != null) {
            /*if (para.getCTP().getPPr().getRPr().getRFonts() != null)
                readRun.getCTR().getRPr().setRFonts(para.getCTP().getPPr().getRPr().getRFonts());
            if (para.getCTP().getPPr().getRPr().getSz() != null)
                readRun.getCTR().getRPr().setSz(para.getCTP().getPPr().getRPr().getSz());
            if (para.getCTP().getPPr().getRPr().getSzCs() != null)
                readRun.getCTR().getRPr().setSzCs(para.getCTP().getPPr().getRPr().getSzCs());*/
            if(para.getCTP().getPPr().getRPr().getRFontsList().size()>0)
                readRun.getCTR().getRPr().setRFontsArray(para.getCTP().getPPr().getRPr().getRFontsArray());
            if(para.getCTP().getPPr().getRPr().getSzCsList().size()>0)
                readRun.getCTR().getRPr().setSzArray(para.getCTP().getPPr().getRPr().getSzArray());
            if(para.getCTP().getPPr().getRPr().getSzCsList().size()>0)
                readRun.getCTR().getRPr().setSzCsArray(para.getCTP().getPPr().getRPr().getSzCsArray());
        }
    }
    public static void setSuPscript (XWPFParagraph para,XWPFRun readRun,String text){
        readRun.setSubscript(VerticalAlign.SUPERSCRIPT);
        setRun(para,readRun,text);
    }
    public static void setSubscript (XWPFParagraph para,XWPFRun readRun,String text){
        readRun.setSubscript(VerticalAlign.SUBSCRIPT);
        setRun(para,readRun,text);
    }

    /**
     * 验证是否本地路径
     *
     * @param path 文件地址
     * @return 是否是网络路径
     */
    private static boolean isLocalPath(String path) {
        return !path.startsWith("http://") && !path.startsWith("https://") && !path.startsWith("ftp://");
    }

    /**
     * 获取文件后缀
     * @param imageUrl
     * @return
     */
    private static String getImageFormat(String imageUrl) {
        String extension = imageUrl.substring(imageUrl.lastIndexOf('.') + 1).toLowerCase();
        switch (extension) {
            case "png":
                return "png";
            case "jpg":
            case "jpeg":
                return "jpg";
            default:
                throw new IllegalArgumentException("不支持的图片格式: " + extension);
        }
    }

    /**
     * 通过文件地址获取文件对象，支持网络图片
     *
     * @param path 文件路径
     * @return 文件对象
     * @throws Exception
     */
    private File getFileByPath(String path) throws Exception {
        if (isLocalPath(path)) {
            return new File(path);
        } else {
            // 如果是网络路径，下载图片并保存到临时文件
            URL imageUrl = new URL(path);
            try (InputStream in = imageUrl.openStream()) {
                BufferedImage image = ImageIO.read(in);
                // 生成临时文件名
                String tempFileName = "temp_" + System.currentTimeMillis() + "." + getImageFormat(path);
                File tempFile = File.createTempFile("temp_", null); // 生成临时文件
                tempFile.deleteOnExit(); // 确保程序退出时删除临时文件
                // 写入临时文件
                ImageIO.write(image, getImageFormat(path), tempFile);
                return tempFile;
            }
        }

    }
    public static List<NutMap> findSupSub(String input) {
        // 匹配上标和下标的正则表达式
        Pattern pattern = Pattern.compile("[\\u2070-\\u209F\\u00B9-\\u00B9\\u00B2-\\u00B3\\u2070-\\u209F\\u2080-\\u2089\\u1D43-\\u1D6A]");

        // 匹配所有的 Unicode 编码的上标字符，但排除下标字符
        Pattern superscriptPattern = Pattern.compile("[\\u2070-\\u2079\\u00B9\\u00B2\\u00B3\\u2074-\\u2079\\u00B9\\u207A-\\u207C\\u207D-\\u207E\\u207F\\u2120\\u1D2C-\\u1D61\\u1D78\\u2C7D\\u1D25-\\u1D2B]");

        // 匹配所有的 Unicode 编码的下标字符，但排除上标字符
        // Pattern subscriptPattern = Pattern.compile("[\\u2080-\\u2089\\u2090-\\u209C\\u1D62-\\u1D6A&&[^\\u2070-\\u2079\\u00B9-\\u00B9\\u00B2-\\u00B3\\u2074-\\u2079\\u00B9-\\u00B9\\u1D43-\\u1D6A]]");
        Pattern subscriptPattern = Pattern.compile("[\\u2080-\\u2089\\u00B0\\u208A-\\u208C\\u208D-\\u208E\\u2090-\\u209C]");

        // 查找匹配的上标
        Matcher inputPattern = pattern.matcher(input);
        int start = 0;
        List<NutMap> strs = new ArrayList<>();
        while (inputPattern.find()) {
            String in = inputPattern.group();
            String normalizedText = Normalizer.normalize(in, Normalizer.Form.NFKC);
            if(inputPattern.start()>0){
                String str = input.substring(start,inputPattern.start());
                if(!str.equals("")) {
                    strs.add(new NutMap().addv("value", str).addv("type", 0));
                }
                if(superscriptPattern.matcher(in).matches()){
                    strs.add(new NutMap().addv("value",normalizedText).addv("type",1));
                }else if(subscriptPattern.matcher(in).matches()){
                    strs.add(new NutMap().addv("value",normalizedText).addv("type",2));
                }else{
                    strs.add(new NutMap().addv("value",in).addv("type",4));
                }
                start = inputPattern.end();
            }
        }

        if(strs.size()==0 || start != (input.length()-1)){
            String str = input.substring(start, input.length());
            if(!str.equals("")) {
                strs.add(new NutMap().addv("value", str).addv("type", 0));
            }
        }
        return strs;
    }
    private static int getPicType(String bookmarkValue){
        int picType = -1;
        if(bookmarkValue.toLowerCase().contains(".emf")){
            picType=Document.PICTURE_TYPE_EMF;
        }else if(bookmarkValue.toLowerCase().contains(".wmf")){
            picType=Document.PICTURE_TYPE_WMF;
        }else if(bookmarkValue.toLowerCase().contains(".pict")){
            picType=Document.PICTURE_TYPE_PICT;
        }else if(bookmarkValue.toLowerCase().contains(".jpeg")){
            picType=Document.PICTURE_TYPE_JPEG;
        }else if(bookmarkValue.toLowerCase().contains(".png")){
            picType=Document.PICTURE_TYPE_PNG;
        }else if(bookmarkValue.toLowerCase().contains(".dib")){
            picType=Document.PICTURE_TYPE_DIB;
        }else if(bookmarkValue.toLowerCase().contains(".gif")){
            picType=Document.PICTURE_TYPE_GIF;
        }else if(bookmarkValue.toLowerCase().contains(".tiff")){
            picType=Document.PICTURE_TYPE_TIFF;
        }else if(bookmarkValue.toLowerCase().contains(".eps")){
            picType=Document.PICTURE_TYPE_EPS;
        }else if(bookmarkValue.toLowerCase().contains(".bmp")){
            picType=Document.PICTURE_TYPE_BMP;
        }else if(bookmarkValue.toLowerCase().contains(".wpg")){
            picType=Document.PICTURE_TYPE_WPG;
        }
        return picType;
    }

    public static String getBookmarkName(CTBookmark bookmark){
        String name = bookmark.getName();
        if(Objects.isNull(name)){
            Node node = bookmark.getDomNode().getFirstChild();
            name = node.getAttributes().getNamedItem(BookMark.BOOKMARK_NAME_ATTR_NAME).getNodeValue();
        }
        return  name;
    }
    public static Integer getBookmarkId(CTBookmark bookmark){
        BigInteger idb = bookmark.getId();
        if(Objects.isNull(idb)){
            Node node = bookmark.getDomNode().getFirstChild();
            String idstr = node.getAttributes().getNamedItem(BookMark.BOOKMARK_ID_ATTR_NAME).getNodeValue();
            return Objects.nonNull(idstr)?Integer.valueOf(idstr):null;
        }else{
            return idb.intValue();
        }
    }

    public static List<CTBookmark>  findBookmarks(Node domNode){
        List<CTBookmark>  bookmarkList = new ArrayList<>();
        try {
            // 查找所有 bookmarkStart 和 bookmarkEnd
            Node currentNode = domNode.getFirstChild();
            while (currentNode != null) {
                if (currentNode.getNodeName().contains(BookMark.BOOKMARK_START_TAG)) {
                    CTBookmark bookmark = CTBookmark.Factory.parse(currentNode);
                    bookmarkList.add(bookmark);
                    String name = getBookmarkName(bookmark);
                    name.trim();
                } else if (currentNode.getNodeName().contains(BookMark.BOOKMARK_END_TAG)) {
                    // 处理 bookmarkEnd
                }
                currentNode = currentNode.getNextSibling();
            }
            /*if(domNode.getLastChild()!=null) {
                bookmarkList.addAll(findBookmarks(domNode.getLastChild()));
            }*/
            if(domNode.getChildNodes().getLength()>0 && bookmarkList.size()==0) {
                for(int i =0 ;i<domNode.getChildNodes().getLength();i++){
                    bookmarkList.addAll(findBookmarks(domNode.getChildNodes().item(i)));
                }
            }
        } catch (XmlException e) {
            throw new RuntimeException(e);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
        return bookmarkList;
    }
}
